/**
 * @license
 * Copyright 2013 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @fileoverview A single avatar.
 * @author blocklygames@neil.fraser.name (Neil Fraser)
 */
'use strict';

goog.provide('Pond.Avatar');

goog.require('Blockly.utils.Coordinate');
goog.require('Blockly.utils.math');
goog.require('BlocklyGames');


Pond.Avatar = class {
  /**
   * Class for a avatar.
   * @param {string} name Avatar's name.
   * @param {!Blockly.utils.Coordinate} startLoc Start location.
   * @param {?number} startDamage Initial damage to avatar (0-100, default 0).
   * @param {boolean} editable Allow user to edit code.
   * @param {!Pond.Battle} battle The battle featuring the avatar.
   */
  constructor(name, startLoc, startDamage, editable, battle) {
    this.name = name;
    this.startLoc_ = startLoc;
    this.startDamage_ = startDamage || 0;
    this.editable = editable;
    this.battle_ = battle;
    /**
     * X/Y location of the avatar (0 - 100).
     * @type Blockly.utils.Coordinate
     */
    this.loc = new Blockly.utils.Coordinate();
    /**
     * Has this avatar fully loaded and started playing?
     */
    this.started = false;
    /**
     * Has the avatar been killed?
     */
    this.dead = false;
    /**
     * Damage of this avatar (0 = perfect, 100 = dead).
     */
    this.damage = 0;
    /**
     * Heading the avatar is moving in (0 - 360).
     */
    this.degree = 0;
    /**
     * Direction the avatar's head is facing (0 - 360).
     */
    this.facing = 0;
    /**
     * Speed the avatar is actually moving (0 - 100).
     */
    this.speed = 0;
    /**
     * Speed the avatar is aiming to move at (0 - 100).
     */
    this.desiredSpeed = 0;
    /**
     * Date of last missile.
     */
    this.lastMissile = 0;

    this.reset();
    this.visualizationIndex = battle.AVATARS.length;
    battle.AVATARS.push(this);
    console.log(this + ' loaded.');
  }

  /**
   * A text representation of this avatar for debugging purposes.
   * @returns {string} String representation.
   */
  toString() {
    return '[' + this.name + ']';
  }

  /**
   * Sets the code for this avatar.
   * @param {string|undefined} blockly Blockly XML, or undefined if no Blockly.
   * @param {string|undefined} js JavaScript source, or undefined if no JS.
   * @param {string} compiled Executable ES5 code (generated by Blockly or Babel).
   */
  setCode(blockly, js, compiled) {
    this.blockly = blockly;
    this.js = js;
    this.compiled = compiled;
  }

  /**
   * Reset this avatar to a starting state.
   */
  reset() {
    this.started = false;
    this.dead = false;
    this.speed = 0;
    this.desiredSpeed = 0;
    this.lastMissile = 0;

    this.damage = this.startDamage_;
    this.loc.x = this.startLoc_.x;
    this.loc.y = this.startLoc_.y;
    // Face the centre.
    this.degree = Pond.Avatar.pointsToAngle(this.loc.x, this.loc.y, 50, 50);
    this.facing = this.degree;
    this.interpreter = null;
  }

  /**
   * Create an interpreter for this avatar.
   */
  initInterpreter() {
    this.interpreter = new Interpreter(this.compiled, this.battle_.initInterpreter);
  }

  /**
   * Damage the avatar.
   * @param {number} add Amount of damage to add.
   */
  addDamage(add) {
    this.damage += add;
    if (this.damage >= 100) {
      this.die();
    }
  }

  /**
   * Kill this avatar.
   */
  die() {
    this.speed = 0;
    this.dead = true;
    this.damage = 100;
    this.battle_.RANK.unshift(this);
    this.battle_.EVENTS.push({'type': 'DIE', 'avatar': this});
    console.log(this + ' sinks.');
  }

  // API functions exposed to the user.

  /**
   * Scan in a direction for the nearest avatar.
   * @param {number} degree Scan in this direction (wrapped to 0-360).
   * @param {number} opt_resolution Sensing resolution, 1 to 20 degrees.
   *   Defaults to 5.
   * @returns {number} Distance (0 - ~141), or Infinity if no avatar detected.
   */
  scan(degree, opt_resolution) {
    let resolution;
    if (opt_resolution === undefined || opt_resolution === null) {
      resolution = 5;
    } else {
      resolution = opt_resolution;
    }
    if ((typeof degree !== 'number') || isNaN(degree) ||
        (typeof resolution !== 'number') || isNaN(resolution)) {
      throw TypeError();
    }
    degree = BlocklyGames.normalizeAngle(degree);
    resolution = Blockly.utils.math.clamp(resolution, 0, 20);

    this.battle_.EVENTS.push({'type': 'SCAN', 'avatar': this,
                              'degree': degree, 'resolution': resolution});

    // Compute both edges of the scan.
    const scan1 = BlocklyGames.normalizeAngle(degree - resolution / 2);
    let scan2 = BlocklyGames.normalizeAngle(degree + resolution / 2);
    if (scan1 > scan2) {
      scan2 += 360;
    }
    const locX = this.loc.x;
    const locY = this.loc.y;
    // Check every enemy for existence in the scan beam.
    let closest = Infinity;
    for (const enemy of this.battle_.AVATARS) {
      if (enemy === this || enemy.dead) {
        continue;
      }
      const ex = enemy.loc.x;
      const ey = enemy.loc.y;
      // Pythagorean theorem to find range to enemy's centre.
      const range = Math.sqrt((ey - locY) * (ey - locY) +
                            (ex - locX) * (ex - locX));
      if (range >= closest) {
        continue;
      }
      // Compute angle between avatar and enemy's centre.
      let angle = Math.atan2(ey - locY, ex - locX);
      angle = BlocklyGames.normalizeAngle(Blockly.utils.math.toDegrees(angle));
      // Raise angle by 360 if needed (handles wrapping).
      if (angle < scan1) {
        angle += 360;
      }
      // Check if enemy is within scan edges.
      if (scan1 <= angle && angle <= scan2) {
        closest = range;
      }
    }
    return closest;
  }

  /**
   * Commands the avatar to move in the specified heading at the specified speed.
   * @param {number} degree Heading (0-360).
   * @param {number} opt_speed Desired speed (0-100).  Defaults to 50.
   */
  drive(degree, opt_speed) {
    let speed;
    if (opt_speed === undefined || opt_speed === null) {
      speed = 50;
    } else {
      speed = opt_speed;
    }
    if ((typeof degree !== 'number') || isNaN(degree) ||
        (typeof speed !== 'number') || isNaN(speed)) {
      throw TypeError;
    }
    const desiredDegree = BlocklyGames.normalizeAngle(degree);
    if (this.degree !== desiredDegree) {
      if (this.speed <= 50) {
        // Changes in direction can be negotiated at speeds of less than 50%.
        this.degree = desiredDegree;
        this.facing = this.degree;
      } else {
        // Stop the avatar if an over-speed turn was commanded.
        speed = 0;
      }
    }
    if (this.speed === 0 && speed > 0) {
      // If starting, bump the speed immediately so that avatars can see a change.
      this.speed = 0.1;
    }
    this.desiredSpeed = Blockly.utils.math.clamp(speed, 0, 100);
  }

  /**
   * Commands the avatar to stop.
   */
  stop() {
    this.desiredSpeed = 0;
  }

  /**
   * Commands the avatar to shoot in the specified heading at the specified range.
   * @param {number} degree Heading (0-360).
   * @param {number} range Distance to impact (0-70).
   * @returns {boolean} True if cannon fired, false if still reloading from a
   *     previous shot.
   */
  cannon(degree, range) {
    if ((typeof degree !== 'number') || isNaN(degree) ||
        (typeof range !== 'number') || isNaN(range)) {
      throw TypeError;
    }
    const now = Date.now();
    if (this.lastMissile + this.battle_.RELOAD_TIME * 1000 > now) {
      return false;
    }
    this.lastMissile = now;
    const startLoc = new Blockly.utils.Coordinate(this.loc.x, this.loc.y);
    degree = BlocklyGames.normalizeAngle(degree);
    this.facing = degree;
    range = Blockly.utils.math.clamp(range, 0, 70);
    const endLoc = new Blockly.utils.Coordinate(
        startLoc.x + Pond.Avatar.angleDx(degree, range),
        startLoc.y + Pond.Avatar.angleDy(degree, range));
    const missile = {
      avatar: this,
      startLoc: startLoc,
      degree: degree,
      range: range,
      endLoc: endLoc,
      progress: 0,
    };
    this.battle_.MISSILES.push(missile);
    this.battle_.EVENTS.push({'type': 'BANG', 'avatar': this,
        'degree': missile.degree});
    return true;
  }

  /**
   * For a given angle and radius, finds the X portion of the offset.
   * Copied from Closure's goog.math.angleDx.
   * @param {number} degrees Angle in degrees (zero points in +X direction).
   * @param {number} radius Radius.
   * @returns {number} The x-distance for the angle and radius.
   */
  static angleDx(degrees, radius) {
    return radius * Math.cos(Blockly.utils.math.toRadians(degrees));
  };

  /**
   * For a given angle and radius, finds the Y portion of the offset.
   * Copied from Closure's goog.math.angleDy.
   * @param {number} degrees Angle in degrees (zero points in +X direction).
   * @param {number} radius Radius.
   * @returns {number} The y-distance for the angle and radius.
   */
  static angleDy(degrees, radius) {
    return radius * Math.sin(Blockly.utils.math.toRadians(degrees));
  }

  /**
   * Computes the angle between two points (x1,y1) and (x2,y2).
   * Angle zero points in the +X direction, 90 degrees points in the +Y
   * direction (down) and from there we grow clockwise towards 360 degrees.
   * Copied from Closure's goog.math.angle.
   * @param {number} x1 x of first point.
   * @param {number} y1 y of first point.
   * @param {number} x2 x of second point.
   * @param {number} y2 y of second point.
   * @returns {number} Standardized angle in degrees of the vector from
   *     x1,y1 to x2,y2.
   */
  static pointsToAngle(x1, y1, x2, y2) {
    const angle = Blockly.utils.math.toDegrees(Math.atan2(y2 - y1, x2 - x1));
    return BlocklyGames.normalizeAngle(angle);
  }
};
